package cn.orangeframe.neon.application.run.http;

import cn.orangeframe.neon.application.run.WorkflowExecutor;
import cn.orangeframe.neon.application.run.http.providers.RestTemplateProvider;
import cn.orangeframe.neon.application.tasks.WorkflowSystemTask;
import cn.orangeframe.neon.domain.enums.TaskStatus;
import cn.orangeframe.neon.domain.enums.TaskType;
import cn.orangeframe.neon.domain.model.TaskModel;
import cn.orangeframe.neon.domain.model.WorkflowModel;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Component
public class HttpTask extends WorkflowSystemTask {

    public static final String REQUEST_PARAMETER_NAME = "http_request";
    static final String MISSING_REQUEST =
            "Missing HTTP request. Task input MUST have a '"
                    + REQUEST_PARAMETER_NAME
                    + "' key with HttpTask.Input as value. See documentation for HttpTask for required input parameters";

    private final TypeReference<Map<String, Object>> mapOfObj = new TypeReference<>() {};
    private final TypeReference<List<Object>> listOfObj = new TypeReference<>() {};
    private final ObjectMapper objectMapper;
    private final RestTemplateProvider restTemplateProvider;

    public HttpTask(ObjectMapper objectMapper, RestTemplateProvider restTemplateProvider) {
        super(TaskType.TASK_TYPE_HTTP);
        this.objectMapper = objectMapper;
        this.restTemplateProvider = restTemplateProvider;
    }

    @Override
    public void execute(WorkflowModel workflow, TaskModel task, WorkflowExecutor executor) {
        log.info("Executing http task: {},{}", task.getTaskReferenceName(), task.getTaskId());
        task.setStatus(TaskStatus.COMPLETED);
        Object request = task.getInputData().get(REQUEST_PARAMETER_NAME);
        if (request == null) {
            task.setReasonForInCompletion(MISSING_REQUEST);
            task.setStatus(TaskStatus.FAILED);
            return;
        }

        Input input = objectMapper.convertValue(request, Input.class);
        if (input.getUri() == null) {
            String reason =
                    "Missing HTTP URI.  See documentation for HttpTask for required input parameters";
            task.setReasonForInCompletion(reason);
            task.setStatus(TaskStatus.FAILED);
            return;
        }

        if (input.getMethod() == null) {
            String reason = "No HTTP method specified";
            task.setReasonForInCompletion(reason);
            task.setStatus(TaskStatus.FAILED);
            return;
        }

        try {
            HttpResponse response = httpCall(input);
            log.debug(
                    "Response: {}, {}, task:{}",
                    response.statusCode,
                    response.body,
                    task.getTaskId());
            if (response.statusCode > 199 && response.statusCode < 300) {
                if (isAsyncComplete(task)) {
                    task.setStatus(TaskStatus.IN_PROGRESS);
                } else {
                    task.setStatus(TaskStatus.COMPLETED);
                }
            } else {
                if (response.body != null) {
                    task.setReasonForInCompletion(response.body.toString());
                } else {
                    task.setReasonForInCompletion("No response from the remote service");
                }
                task.setStatus(TaskStatus.FAILED);
            }
            //noinspection ConstantConditions
            if (response != null) {
                task.addOutput("response", response.asMap());
            }

        } catch (Exception e) {
            log.error(
                    "Failed to invoke {} task: {} - uri: {}, vipAddress: {} in workflow: {}",
                    getTaskType(),
                    task.getTaskId(),
                    input.getUri(),
                    input.getVipAddress(),
                    task.getWorkflowInstanceId(),
                    e);
            task.setStatus(TaskStatus.FAILED);
            task.setReasonForInCompletion(
                    "Failed to invoke " + getTaskType() + " task due to: " + e);
            task.addOutput("response", e.toString());
        }
    }

    /**
     * @param input HTTP Request
     * @return Response of the http call
     * @throws Exception If there was an error making http call Note: protected access is so that
     *     tasks extended from this task can re-use this to make http calls
     */
    protected HttpResponse httpCall(Input input) throws Exception {
        RestTemplate restTemplate = restTemplateProvider.getRestTemplate(input);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.valueOf(input.getContentType()));
        headers.setAccept(Collections.singletonList(MediaType.valueOf(input.getAccept())));

        input.headers.forEach(
                (key, value) -> {
                    if (value != null) {
                        headers.add(key, value.toString());
                    }
                });

        HttpEntity<Object> request = new HttpEntity<>(input.getBody(), headers);

        HttpResponse response = new HttpResponse();
        try {
            ResponseEntity<String> responseEntity = restTemplate.exchange(input.getUri(), input.getMethod(), request, String.class);
            if (responseEntity.getStatusCode().is2xxSuccessful() && responseEntity.hasBody()) {
                response.body = extractBody(responseEntity.getBody());
            }

            HttpStatusCode statusCode = responseEntity.getStatusCode();

            if (statusCode instanceof HttpStatus httpStatus) {
                response.statusCode = httpStatus.value();
                response.reasonPhrase = httpStatus.getReasonPhrase();
            } else {
                response.statusCode = responseEntity.getStatusCode().value();
                response.reasonPhrase = String.valueOf(responseEntity.getStatusCode().value());
            }
            response.headers = responseEntity.getHeaders();
            return response;
        } catch (RestClientException ex) {
            log.error(
                    String.format(
                            "Got unexpected http response - uri: %s, vipAddress: %s",
                            input.getUri(), input.getVipAddress()),
                    ex);
            String reason = ex.getLocalizedMessage();
            log.error(reason, ex);
            throw new Exception(reason);
        }
    }

    private Object extractBody(String responseBody) {
        try {
            JsonNode node = objectMapper.readTree(responseBody);
            if (node.isArray()) {
                return objectMapper.convertValue(node, listOfObj);
            } else if (node.isObject()) {
                return objectMapper.convertValue(node, mapOfObj);
            } else if (node.isNumber()) {
                return objectMapper.convertValue(node, Double.class);
            } else {
                return node.asText();
            }
        } catch (IOException jpe) {
            log.error("Error extracting response body", jpe);
            return responseBody;
        }
    }

    public static class HttpResponse {

        public Object body;
        public MultiValueMap<String, String> headers;
        public int statusCode;
        public String reasonPhrase;

        @Override
        public String toString() {
            return "HttpResponse [body="
                    + body
                    + ", headers="
                    + headers
                    + ", statusCode="
                    + statusCode
                    + ", reasonPhrase="
                    + reasonPhrase
                    + "]";
        }

        public Map<String, Object> asMap() {
            Map<String, Object> map = new HashMap<>();
            map.put("body", body);
            map.put("headers", headers);
            map.put("statusCode", statusCode);
            map.put("reasonPhrase", reasonPhrase);
            return map;
        }
    }

    @Getter
    @Setter
    public static class Input {

        private HttpMethod method; // PUT, POST, GET, DELETE, OPTIONS, HEAD
        private String vipAddress;
        private String appName;
        private Map<String, Object> headers = new HashMap<>();
        private String uri;
        private Object body;
        private String accept = MediaType.APPLICATION_JSON_VALUE;
        private String contentType = MediaType.APPLICATION_JSON_VALUE;
        private Integer connectionTimeOut;
        private Integer readTimeOut;
    }
}
